#include "Physics.h"

#include <algorithm>
using std::min;
using std::max;

//#include <cstring>

#include <strings.h>

#include "../../math/Math.h"
#include "../../math/Random.h"
#include "../../qt/Logger.h"

#include "World.h"

//#define UPDATE_COEFFICIENT_ENERGY_THRESHOLD   0.0
//#define UPDATE_COEFFICIENT_ADHESION           0.0


//#define UPDATE_COEFFICIENT_BORDER_COUNT       1.0
//#define UPDATE_COEFFICIENT_VOLUME_DIFFERENCE  0.5
//#define UPDATE_COEFFICIENT_CONVEXITY          (-12.0)
//#define UPDATE_CONVEXITY_BASE                 4

/*
	Physics
*/

/*	public	*/

Physics::Physics(World& world) :
	_parameters(),
	_world(world),
	_iterator(world) {}

void Physics::run(int nIterations) {
	for(int i = 0; i != nIterations; ++i) {
		runOnce();
	}
}

void Physics::_updateCells() {
	int nCells = _world.getMaxCellId() + 1;
	for(int i = 0; i != nCells; ++i) {

		/* cell */
		Cell* cell = _world.getCell(i);

		if (cell != 0) {
			/* run */
			cell->update();
		}
	}
}

void Physics::runOnce() {
//	Logger::debug(">>> Physics::runOnce()");
	/* random */
	Random random = Random::getInstance();

	/* run cell controllers */
//	_runCellControllers();
	_updateCells();

	/* iterator step init */
	_iterator.startStep();

	int xTile;
	int yTile;
	int xNeighbour;
	int yNeighbour;

//	Logger::debug(QString("\tbounding box width  : %1").arg(_world._boundingBoxWidth));
//	Logger::debug(QString("\tbounding box height : %1").arg(_world._boundingBoxHeight));
	int debugIterations = 0;

	while(_iterator.getTilePair(&xTile, &yTile, &xNeighbour, &yNeighbour)) {

		++debugIterations;

		/* tile id */
		CellId tileId = _world.grid(xTile, yTile);

		/* neighbour id */
		CellId neighbourId = _world.grid(xNeighbour, yNeighbour);

		/* update probability */
		double updateProbability = _getUpdateProbability(xNeighbour, yNeighbour, tileId);

		/* update ? */
		if (random.getBool(updateProbability)) {
			/* ...yes */

			/* if tile is a cell, grow the cell */
			if (tileId != 0) {
				Cell* cell = _world.getCell(tileId);
				cell->grow();
			}

			/* if neighbour is a cell, shrink the */
			if (neighbourId != 0) {
				Cell* neighbourCell = _world.getCell(neighbourId);
				neighbourCell->shrink();
			}

			/* update world */
			_world.setTile(xNeighbour, yNeighbour, tileId);
		}
	}

//	Logger::debug(QString("\tbounding box width  : %1").arg(_world._boundingBoxWidth));
//	Logger::debug(QString("\tbounding box height : %1").arg(_world._boundingBoxHeight));

//	Logger::debug(QString("\titeration : %1").arg(debugIterations));

//	Logger::debug("<<< Physics::runOnce()");
}

PhysicsParameters& Physics::getParameters() {
	return _parameters;
}

/*	private	*/

double Physics::_getAdhesionIdId(CellId id1, CellId id2) {

	if (id1 == id2) {
		/* ignore intra-cell or substrate/substrate interactions */
		return 0;

	} else if (id1 == 0 || id2 == 0) {
		/* cell-substrate interaction */

		/* cell */
		Cell const* cell;

		/* cell initialisation */
		if (id1 == 0) {
			/* id1 : substrate */

			/* id2 : cell */
			cell = _world.getCell(id2);
		} else {
			/* id2 : substrate */
			assert(id2 == 0);

			/* id : cell */
			cell = _world.getCell(id1);
		}

		return cell->getAdhesionToSubstrate();
	} else {
		/* cell-cell interaction */

		/* cells */
		Cell const& cell1 = *(_world.getCell(id1));
		Cell const& cell2 = *(_world.getCell(id2));

		return cell1.getAdhesionToCell(cell2);
	}
}

double Physics::_getAdhesionSum(int xPosition, int yPosition, CellId centralId) {

	/* sum */
	double sum = 0.0;

	/* adhesion */
	sum += _getAdhesionIdId(centralId, _world.grid(xPosition    , yPosition - 1)); /* top      */
	sum += _getAdhesionIdId(centralId, _world.grid(xPosition + 1, yPosition    )); /* right    */
	sum += _getAdhesionIdId(centralId, _world.grid(xPosition    , yPosition + 1)); /* bottom   */
	sum += _getAdhesionIdId(centralId, _world.grid(xPosition - 1, yPosition    )); /* left     */

	return sum;
}

bool Physics::_isUpdateValid(int xPosition, int yPosition) {
	/* no splitting */
	//FIX bug: says splitting when e.g. E & NW

	CellId cellId = _world.grid(xPosition, yPosition);

//	/* overwriting the substrate is always valid */
//	if (cellId == 0) {
//		return true;
//	}

	/* neighbouring tiles */
	bool neighbours[8];

	neighbours[0] = _world.grid(xPosition    , yPosition - 1) == cellId; /* N  */
	neighbours[1] = _world.grid(xPosition + 1, yPosition - 1) == cellId; /* NW */
	neighbours[2] = _world.grid(xPosition + 1, yPosition    ) == cellId; /* W  */
	neighbours[3] = _world.grid(xPosition + 1, yPosition + 1) == cellId; /* SW */
	neighbours[4] = _world.grid(xPosition    , yPosition + 1) == cellId; /* S  */
	neighbours[5] = _world.grid(xPosition - 1, yPosition + 1) == cellId; /* SE */
	neighbours[6] = _world.grid(xPosition - 1, yPosition    ) == cellId; /* E  */
	neighbours[7] = _world.grid(xPosition - 1, yPosition - 1) == cellId; /* NE */

	int separations = 0;
	bool last = neighbours[7];
	int i = 0;
	while(i != 8) {
		bool current = neighbours[i];
		if (current != last) {
			++separations;
			last = current;
		}

		++i;
	}

	return separations <= 3;
}

double Physics::_getUpdateProbability(int xPosition, int yPosition, CellId cellId) {

	/* current cell Id */
	CellId currentCellId = _world.grid(xPosition, yPosition);

	/* valid update ? */
	if (!_isUpdateValid(xPosition, yPosition)) {
		/* no */
		return 0.0;
	}

	/* energies */
	double energyCurrent;
	double energyIf;

	if (cellId == 0) {
		/* overwriting a cell with the substrate */

		/* current cell there */
		Cell& cell = *(_world.getCell(currentCellId));

		energyCurrent = cell.getEnergy(_parameters, xPosition, yPosition);
		energyIf      = cell.getEnergyIfRemoved(_parameters, xPosition, yPosition);
	} else {
		if (currentCellId == 0) {
			/* overwriting the substrate with a cell */

			Cell& cell = *(_world.getCell(cellId));

			energyCurrent = cell.getEnergy(_parameters, xPosition, yPosition);
			energyIf      = cell.getEnergyIfAdded(_parameters, xPosition, yPosition);
		} else {
			/* overwriting a cell with another cell */

			Cell& currentCell = *(_world.getCell(currentCellId));
			Cell& cell        = *(_world.getCell(cellId));

			energyCurrent = currentCell.getEnergy(_parameters, xPosition, yPosition)
					+ cell.getEnergy(_parameters, xPosition, yPosition);
			energyIf = currentCell.getEnergyIfRemoved(_parameters, xPosition, yPosition)
				+ cell.getEnergyIfAdded(_parameters, xPosition, yPosition);
		}
	}

	/* udpate energy threshold */
	double updateEnergyThreshold = _parameters.getUpdateEnergyThreshold();
	double adhesionEnergyCoef = _parameters.getAdhesionEnergyCoef();

	/* adhesion */
	energyCurrent += adhesionEnergyCoef * _getAdhesionSum(xPosition, yPosition, currentCellId);
	energyIf      += adhesionEnergyCoef * _getAdhesionSum(xPosition, yPosition, cellId);
//	Logger::debug(QString("\t energyCurrent = %1").arg(energyCurrent));
//	Logger::debug(QString("\t energyIf      = %1").arg(energyIf));

	/* energy delta */
	double energyDelta = energyIf - energyCurrent;

	/* probability */
	if (energyDelta <= updateEnergyThreshold) {
		return 1.0;
	} else {
		return Math::exp(-(energyDelta - updateEnergyThreshold));
	}
}

